1 /**
2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This module contains an analyser and runner for
7 [iwyu](https://github.com/include-what-you-use/include-what-you-use).
8 */
9 module code_checker.engine.builtin.include_what_you_use;
10 
11 import logger = std.experimental.logger;
12 import std.concurrency : Tid, thisTid;
13 import std.exception : collectException;
14 import std.typecons : Tuple;
15 
16 import my.path : AbsolutePath, Path;
17 
18 import code_checker.engine.builtin.clang_tidy_classification : CountErrorsResult;
19 import code_checker.engine.file_filter;
20 import code_checker.engine.types;
21 import code_checker.process : RunResult;
22 
23 @safe:
24 
25 class IncludeWhatYouUse : BaseFixture {
26     private {
27         Environment env;
28         Result result_;
29         string[] iwyuArgs;
30     }
31 
32     override string name() {
33         return "iwyu";
34     }
35 
36     override string explain() {
37         return "using iwyu (include what you use)";
38     }
39 
40     /// The environment the analyzers execute in.
41     override void putEnv(Environment v) {
42         this.env = v;
43     }
44 
45     /// Setup the environment for analyze.
46     override void setup() {
47         import std.algorithm : copy, map, joiner;
48         import std.array : appender;
49         import std.file : exists;
50         import std.range : put, only;
51         import code_checker.utility : replaceConfigWords, warnIfFileDoNotExist;
52 
53         auto app = appender!(string[])();
54         app.put(env.conf.iwyu.binary);
55         only(env.conf.iwyu.maps, env.conf.iwyu.defaultMaps).joiner.replaceConfigWords.warnIfFileDoNotExist.map!(
56                 a => only("-Xiwyu", "--mapping_file=" ~ a)).joiner.copy(app);
57         env.conf.iwyu.extraFlags.copy(app);
58         iwyuArgs = app.data;
59     }
60 
61     override void execute() {
62         result_.status = Status.passed;
63         executeParallel(env, iwyuArgs, result_);
64     }
65 
66     override void tearDown() {
67     }
68 
69     override Result result() {
70         return result_;
71     }
72 }
73 
74 private:
75 
76 void executeParallel(Environment env, string[] iwyuArgs, ref Result result_) @safe {
77     import std.algorithm : copy, map, joiner;
78     import std.array : appender, array;
79     import std.concurrency : thisTid, receiveTimeout;
80     import std.file : exists;
81     import std.format : format;
82     import std.parallelism : task, TaskPool;
83     import code_checker.engine.compile_db;
84     import code_checker.engine.logger : Logger;
85 
86     bool logged_failure;
87     auto logg = Logger(env.conf.logg.dir);
88 
89     void collectResult(immutable(IwyuResult)* res_) @trusted nothrow {
90         import std.typecons : nullableRef;
91         import colorlog;
92 
93         auto res = nullableRef(cast() res_);
94 
95         logger.infof("%s '%s'", "iwyu analyzing".color(Color.yellow)
96                 .bg(Background.black), res.file).collectException;
97 
98         // seems like 2 also means OK.
99         const allIsOk = res.exitStatus == 0 || res.exitStatus == 2;
100 
101         if (!allIsOk)
102             result_.score -= res.exitStatus > 0 ? res.exitStatus : 0;
103 
104         if (!allIsOk) {
105             result_.failed ~= res.file;
106             res.print;
107 
108             if (env.conf.logg.toFile) {
109                 try {
110                     const logFile = Path(res.file.toString ~ ".iwyu").AbsolutePath;
111                     logg.put(logFile, [res.output]);
112                 } catch (Exception e) {
113                     logger.warning(e.msg).collectException;
114                     logger.warning("Unable to log to file").collectException;
115                 }
116             }
117 
118             if (!logged_failure) {
119                 result_.msg ~= Msg(MsgSeverity.failReason, "iwyu suggested improvements");
120                 logged_failure = true;
121             }
122 
123             try {
124                 result_.msg ~= Msg(MsgSeverity.improveSuggestion,
125                         format("iwyu: %s in %s", res.exitStatus, res.file));
126             } catch (Exception e) {
127                 logger.warning(e.msg).collectException;
128                 logger.warning("Unable to add user message to the result").collectException;
129             }
130 
131             result_.status = mergeStatus(result_.status, Status.failed);
132         }
133     }
134 
135     auto pool = new TaskPool;
136     scope (exit)
137         pool.finish;
138     ExpectedReplyCounter cond;
139 
140     auto file_filter = FileFilter(env.conf.staticCode.fileExcludeFilter);
141     auto fixedDb = toRange(env);
142 
143     foreach (cmd; fixedDb) {
144         if (!exists(cmd.cmd.absoluteFile.toString)) {
145             result_.score -= 1000;
146             result_.msg ~= Msg(MsgSeverity.failReason, "iwyu where unable to find one of the specified files in compile_commands.json on the filesystem. Your compile_commands.json is probably out of sync. Regenerate it.");
147             continue;
148         } else if (!file_filter.match(cmd.cmd.absoluteFile)) {
149             continue;
150         }
151 
152         cond.expected++;
153 
154         immutable(IwyuWork)* w = () @trusted {
155             auto args = appender!(string[])();
156             iwyuArgs.copy(args);
157             cmd.flags.systemIncludes.map!(a => ["-isystem", a]).joiner.copy(args);
158             cmd.flags.includes.map!(a => ["-I", a]).joiner.copy(args);
159             args.put(cmd.cmd.absoluteFile);
160 
161             return cast(immutable) new IwyuWork(args.data, cmd.cmd.absoluteFile);
162         }();
163 
164         auto t = task!taskIwyu(thisTid, w);
165         pool.put(t);
166     }
167 
168     while (cond.isWaitingForReplies) {
169         import core.time : dur;
170 
171         () @trusted {
172             try {
173                 if (receiveTimeout(1.dur!"seconds", &collectResult)) {
174                     cond.replies++;
175                 }
176             } catch (Exception e) {
177                 logger.error(e.msg);
178             }
179         }();
180     }
181 }
182 
183 struct ExpectedReplyCounter {
184     int expected;
185     int replies;
186 
187     bool isWaitingForReplies() {
188         return replies < expected;
189     }
190 }
191 
192 struct IwyuResult {
193     AbsolutePath file;
194 
195     /// Exit status from running iwyu.
196     int exitStatus;
197     /// Captured output from iwyu.
198     string[] output;
199 
200     void print() @safe nothrow const scope {
201         import std.stdio : writeln;
202 
203         foreach (l; output)
204             writeln(l).collectException;
205     }
206 }
207 
208 struct IwyuWork {
209     string[] args;
210     AbsolutePath file;
211 }
212 
213 void taskIwyu(Tid owner, immutable IwyuWork* work_) nothrow @trusted {
214     import std.concurrency : send;
215     import code_checker.process;
216 
217     IwyuWork* work = cast(IwyuWork*) work_;
218     auto rval = new IwyuResult;
219     rval.file = work.file;
220 
221     try {
222         auto res = run(work.args);
223         rval.exitStatus = res.status;
224         rval.output = res.stdout ~ res.stderr;
225     } catch (Exception e) {
226         logger.warning(e.msg).collectException;
227     }
228 
229     while (true) {
230         try {
231             owner.send(cast(immutable) rval);
232             break;
233         } catch (Exception e) {
234             logger.tracef("failed sending to: %s", owner).collectException;
235         }
236     }
237 }